﻿namespace Ulid;

/// <summary>
/// 这是一个用于 ULID 随机部分的随机数生成器（RNG），它利用给定日期的微秒数来确定 ULID 最终的 “随机” 部分。
/// </summary>
public class MicrosecondUlidRng : IUlidRng
{
    private readonly IUlidRng _rng;
    private readonly ushort _mask;
    private readonly byte _negmaskmsb;
    private readonly byte _negmasklsb;

    /// <summary>
    /// 初始化 <see cref="MicrosecondUlidRng"/> 类的一个新实例
    /// </summary>
    /// <param name="rng">用于获取随机数的 <see cref="IUlidRng"/></param>
    /// <param name="bits">用于微秒部分的字节位数</param>
    /// <exception cref="ArgumentNullException">当 <paramref name="rng"/> 为空时抛出</exception>
    public MicrosecondUlidRng(IUlidRng rng, int bits = 14)
    {
        _rng = rng ?? throw new ArgumentNullException(nameof(rng));
        if (bits is < 0 or > 14)
        {
            throw new ArgumentOutOfRangeException(nameof(bits));
        }

        // 预计算掩码
        _mask = (ushort)(-1 << 15 - bits);  // 为微秒部分预留的字节位数再加 1，以便让随机部分产生溢出

        _negmaskmsb = (byte)(~_mask >> 8);
        _negmasklsb = (byte)(~_mask & 0xFF);
    }

    public byte[] GetRandomBytes(DateTimeOffset dateTime)
    {
        var buffer = _rng.GetRandomBytes(dateTime);
        Core(buffer, dateTime);
        return buffer;
    }

    public void GetRandomBytes(Span<byte> buffer, DateTimeOffset dateTime)
    {
        if (buffer.Length < BaseUlidRng.RANDLEN)
        {
            Throw(buffer.Length);
            static void Throw(int len) => throw new ArgumentException($"The given buffer must be at least {BaseUlidRng.RANDLEN} bytes long, actual: {len}");
        }

        _rng.GetRandomBytes(buffer, dateTime);
        Core(buffer, dateTime);
    }

    private void Core(Span<byte> buffer, DateTimeOffset dateTime)
    {
        // 从时间戳中提取微秒数（最多 14 位），通过向左移动 2 位，将微秒数的最高有效位（14 位）与无符号短整型（ushort）的最高有效位（16 位）对齐。
        // 然后屏蔽掉不需要的位。
        var usecpart = (ushort)(dateTime.Ticks % 10000 << 2 & _mask);

        // 现在我们已经从最高有效位（MSB）开始获得了所需位数的微秒部分数据；用微秒部分的数据覆盖随机数据的前两个字节。
        buffer[0] = (byte)(buffer[0] & _negmaskmsb | usecpart >> 8);
        buffer[1] = (byte)(buffer[0] & _negmasklsb | usecpart & 0xFF);
    }
}
